Bank Account Fraud Dataset Suite (NeurIPS 2022)¶

El conjunto de datos de Fraude en Cuentas Bancarias (BAF) ha sido publicado en NeurIPS 2022 y consta de un total de 6 conjuntos de datos tabulares sintéticos de fraudes en cuentas bancarias diferentes. BAF es un banco de pruebas realista, completo y sólido para evaluar métodos novedosos y existentes en aprendizaje automático y aprendizaje automático justo, y es el primero de su tipo.

Estos conjuntos de datos son:

  1. Realistas, basados en un conjunto de datos del mundo real actual para la detección de fraudes.
  2. Sesgados, cada conjunto de datos tiene tipos de sesgo controlados distintos.
  3. Desbalanceados, esta configuración presenta una prevalencia extremadamente baja de la clase positiva.
  4. Dinámicos, con datos temporales y cambios en la distribución observados.
  5. Preservadores de la privacidad, para proteger la identidad de posibles solicitantes, hemos aplicado técnicas de privacidad diferencial (adición de ruido), codificación de características y entrenado un modelo generativo (CTGAN).

Definición del problema¶

Se desea predecir los fraudes en la apertura de cuentas bancarias en línea. Por tanto, todas las variables del dataset que se empleen para el modelo, deben poderse utilizar en el momento de su llamada. Para ello, se empleará un algoritmo de clasificación supervisado.

Los pasos a realizar son:

  1. Análisis inicial de los datos y preprocesamiento inicial
  2. Correlaciones, tratamiento de missing y outliers
  3. Tratamiento de variables categoricas: encoding
  4. Aplicación de algoritmos
  5. Evaluación con la muestra de test

Importar librerias¶

In [1]:
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
import plotly.express as px

pd.set_option('display.max_columns', 500)
pd.set_option('display.max_rows', 5000)

Funciones¶

In [2]:
def dame_variables_categoricas(dataset=None):
    '''
    ----------------------------------------------------------------------------------------------------------
    Función dame_variables_categoricas:
    ----------------------------------------------------------------------------------------------------------
        -Descripción: Función que recibe un dataset y devuelve una lista con los nombres de las 
        variables categóricas
        -Inputs: 
            -- dataset: Pandas dataframe que contiene los datos
        -Return:
            -- lista_variables_categoricas: lista con los nombres de las variables categóricas del
            dataset de entrada con menos de 100 valores diferentes
            -- 1: la ejecución es incorrecta
    '''
    if dataset is None:
        print(u'\nFaltan argumentos por pasar a la función')
        return 1
    lista_variables_categoricas = []
    other = []
    for i in dataset.columns:
        if (dataset[i].dtype!=float) & (dataset[i].dtype!=int):
            unicos = int(len(np.unique(dataset[i].dropna(axis=0, how='all'))))
            if unicos < 100:
                lista_variables_categoricas.append(i)
            else:
                other.append(i)

    return lista_variables_categoricas, other
In [3]:
path_folder = "./"
df_base = pd.read_csv(path_folder +"Base.csv", low_memory=False)
df_base.shape
Out[3]:
(1000000, 32)

Entendemos que el modelo se va a ejecutar cuando los clientes se den de alta en la empresa. Con este modelo queremos detectar si un cliente si es fraudulento o no. El siguiente paso es analizar todas las variables del dataset e identificar las posibles variables futuras. Después de analizarlas podemos afirmar que las variables presentadas parecen ser características o atributos que describen diferentes aspectos de una solicitud o aplicación, algunas de las cuales pueden ser indicadores o características potenciales para predecir si una solicitud es fraudulenta o no. Esto quiere decir que no encontramos variables a futuro.

Analisis general de la tabla¶

Dimensión¶

In [4]:
print(df_base.shape, df_base.drop_duplicates().shape)
(1000000, 32) (1000000, 32)

Esto significa que el dataset esta compuesto de 1 millón de registros y se compone de un total de 32 columnas. Al obtener dos veces el mismo resultado, podemos confirmar que no hay columnas duplicadas.

Tipos de datos¶

In [5]:
df_base.dtypes.to_dict()
Out[5]:
{'fraud_bool': dtype('int64'),
 'income': dtype('float64'),
 'name_email_similarity': dtype('float64'),
 'prev_address_months_count': dtype('int64'),
 'current_address_months_count': dtype('int64'),
 'customer_age': dtype('int64'),
 'days_since_request': dtype('float64'),
 'intended_balcon_amount': dtype('float64'),
 'payment_type': dtype('O'),
 'zip_count_4w': dtype('int64'),
 'velocity_6h': dtype('float64'),
 'velocity_24h': dtype('float64'),
 'velocity_4w': dtype('float64'),
 'bank_branch_count_8w': dtype('int64'),
 'date_of_birth_distinct_emails_4w': dtype('int64'),
 'employment_status': dtype('O'),
 'credit_risk_score': dtype('int64'),
 'email_is_free': dtype('int64'),
 'housing_status': dtype('O'),
 'phone_home_valid': dtype('int64'),
 'phone_mobile_valid': dtype('int64'),
 'bank_months_count': dtype('int64'),
 'has_other_cards': dtype('int64'),
 'proposed_credit_limit': dtype('float64'),
 'foreign_request': dtype('int64'),
 'source': dtype('O'),
 'session_length_in_minutes': dtype('float64'),
 'device_os': dtype('O'),
 'keep_alive_session': dtype('int64'),
 'device_distinct_emails_8w': dtype('int64'),
 'device_fraud_count': dtype('int64'),
 'month': dtype('int64')}

A través de esta función hemos querido obtener la tipología de las variables que componen el dataset.

Exploración de la variable objetivo y tratamiento¶

In [6]:
porcentaje_fraude = df_base['fraud_bool'].value_counts(normalize=True) * 100

recuento_fraude = df_base['fraud_bool'].value_counts()

tabla_fraude = pd.DataFrame({'Recuento': recuento_fraude, 'Porcentaje (%)': porcentaje_fraude})
tabla_fraude.index.name = 'Valor de fraud_bool'
tabla_fraude = tabla_fraude.reset_index()
tabla_fraude
Out[6]:
Valor de fraud_bool Recuento Porcentaje (%)
0 0 988971 98.8971
1 1 11029 1.1029

Con esta tabla que hemos creado, hemos observado que casi el 99% del dataset estarían identificados como clientes no fraudulentos. En cambio, el 1% restante serían considerados clientes fraudulentos. Esta variables se encuentra muy descompensada, por lo que en un futuro, habría que hacer un oversampling o undersampling.

In [7]:
fig = px.histogram(tabla_fraude, x='Valor de fraud_bool', y=['Porcentaje (%)'])
fig.show()

Para que se pueda apreciar la descompensación de la variable 'fraud_bool', hemos querido hacer una representación gráfica de la misma.

Selección de threshold por filas y columnas para eliminar valores missing¶

In [8]:
null_columns = df_base.isnull().sum().sort_values(ascending=False)
null_rows = df_base.isnull().sum(axis=1).sort_values(ascending=False)
print(null_columns.shape, null_rows.shape)
(32,) (1000000,)
In [9]:
df_null_columnas = pd.DataFrame(null_columns, columns=['nulos_columnas'])     
df_null_filas = pd.DataFrame(null_rows, columns=['nulos_filas'])  
df_null_filas['target'] = df_base['fraud_bool'].copy()
df_null_columnas['porcentaje_columnas'] = df_null_columnas['nulos_columnas']/df_base.shape[0]
df_null_filas['porcentaje_filas']= df_null_filas['nulos_filas']/df_base.shape[1]

Este análisis explica cómo los valores nulos están previamente tratados, y queríamos confirmar la descricpión del dataset: no hay valores 0 ni nulos. Por otro lado, vamos a analizar tanto las filas como las columnas para comprobar que no hay nulos ni valores 0.

In [10]:
df_null_columnas
Out[10]:
nulos_columnas porcentaje_columnas
fraud_bool 0 0.0
income 0 0.0
device_fraud_count 0 0.0
device_distinct_emails_8w 0 0.0
keep_alive_session 0 0.0
device_os 0 0.0
session_length_in_minutes 0 0.0
source 0 0.0
foreign_request 0 0.0
proposed_credit_limit 0 0.0
has_other_cards 0 0.0
bank_months_count 0 0.0
phone_mobile_valid 0 0.0
phone_home_valid 0 0.0
housing_status 0 0.0
email_is_free 0 0.0
credit_risk_score 0 0.0
employment_status 0 0.0
date_of_birth_distinct_emails_4w 0 0.0
bank_branch_count_8w 0 0.0
velocity_4w 0 0.0
velocity_24h 0 0.0
velocity_6h 0 0.0
zip_count_4w 0 0.0
payment_type 0 0.0
intended_balcon_amount 0 0.0
days_since_request 0 0.0
customer_age 0 0.0
current_address_months_count 0 0.0
prev_address_months_count 0 0.0
name_email_similarity 0 0.0
month 0 0.0
In [14]:
threshold=0.9
list_vars_not_null = list(df_null_columnas[df_null_columnas['porcentaje_columnas']<threshold].index)
df_base_filter_null = df_base.loc[:, list_vars_not_null]
df_base_filter_null.shape
Out[14]:
(1000000, 32)
In [13]:
df_null_filas
Out[13]:
nulos_filas target porcentaje_filas
0 0 1 0.0
666657 0 0 0.0
666659 0 0 0.0
666660 0 0 0.0
666661 0 0 0.0
... ... ... ...
333337 0 0 0.0
333338 0 0 0.0
333339 0 0 0.0
333340 0 0 0.0
999999 0 0 0.0

1000000 rows × 3 columns

In [10]:
variables = ['prev_address_months_count', 'intended_balcon_amount', 'bank_months_count', 'session_length_in_minutes', 'device_distinct_emails_8w']
missing_count = []
missing_percentage = []

for variable in variables:
    if variable == 'intended_balcon_amount':
        missing_count.append(df_base[df_base[variable] < 0].shape[0])
    else:
        missing_count.append(df_base[df_base[variable] == -1].shape[0])

    missing_percentage.append((missing_count[-1] / len(df_base)) * 100)

missing_data_df = pd.DataFrame({
    'Variable': variables,
    'Missing': missing_count,
    'Porcentaje Missing': missing_percentage
})

missing_data_df
Out[10]:
Variable Missing Porcentaje Missing
0 prev_address_months_count 712920 71.2920
1 intended_balcon_amount 742523 74.2523
2 bank_months_count 253635 25.3635
3 session_length_in_minutes 2015 0.2015
4 device_distinct_emails_8w 359 0.0359

Podemos observar cómo en estas cinco variables existen valores nulos pero, como han sido tratados previamente no afectan al análisis de nuestro dataset. Las variables que más valores de este tipo incluyen son 'prev_address_months_count' y 'intended_balcon_amount'

Tipos: Variables categóricas y numéricas¶

In [17]:
list_cat_vars, other = dame_variables_categoricas(dataset=df_base_filter_null)
df_base_filter_null[list_cat_vars] = df_base_filter_null[list_cat_vars].astype("category")
df_base_filter_null[list_cat_vars].head()
Out[17]:
fraud_bool device_fraud_count device_distinct_emails_8w keep_alive_session device_os source foreign_request has_other_cards bank_months_count phone_mobile_valid phone_home_valid housing_status email_is_free employment_status date_of_birth_distinct_emails_4w payment_type customer_age month
0 1 0 1 0 windows INTERNET 0 0 24 0 1 BA 0 CA 6 AA 50 7
1 1 0 1 0 windows INTERNET 0 0 15 0 0 BA 1 CA 3 AB 50 7
2 1 0 1 0 other INTERNET 0 0 -1 1 0 BA 1 CB 14 AC 40 7
3 1 0 1 0 linux INTERNET 0 1 31 1 0 BA 1 CA 6 AB 50 7
4 1 0 1 1 macintosh INTERNET 0 0 31 0 1 BA 1 CA 2 AB 50 7

En este apartado hemos seleccionado únicamente las variables de tipo categóricas que ya están tratadas.

In [22]:
len(other)
Out[22]:
5
In [15]:
list_cat_vars
Out[15]:
['fraud_bool',
 'device_fraud_count',
 'device_distinct_emails_8w',
 'keep_alive_session',
 'device_os',
 'source',
 'foreign_request',
 'has_other_cards',
 'bank_months_count',
 'phone_mobile_valid',
 'phone_home_valid',
 'housing_status',
 'email_is_free',
 'employment_status',
 'date_of_birth_distinct_emails_4w',
 'payment_type',
 'customer_age',
 'month']
In [18]:
df_base_filter_null['source'].value_counts()
Out[18]:
source
INTERNET    992952
TELEAPP       7048
Name: count, dtype: int64

Esta variable, como podemos observar, hace referencia a una clasificación basada en que, cuando te das de alta, existen dos medios para hacerlo: internet o la app. Los resultados reflejan que una mayor cantidad de usuarios se decantan por hacerlo a través de internet.

In [19]:
df_base_filter_null['device_os'].value_counts()
Out[19]:
device_os
other        342728
linux        332712
windows      263506
macintosh     53826
x11            7228
Name: count, dtype: int64

Esta variable nos ha parecido interesante estudiar su categoría porque muestra el sistema operativo a través del cual han realizado la petición. Nos ha llamado la atención especialmente que haya datos que no han sido capaces de detectar su sistema en 'other', así como el gran uso de Linux.

In [19]:
df_base_filter_null[other].head(10)
Out[19]:
credit_risk_score bank_branch_count_8w zip_count_4w current_address_months_count prev_address_months_count
0 185 1 769 88 -1
1 259 718 366 144 -1
2 177 1 870 132 -1
3 110 1921 810 22 -1
4 295 1990 890 218 -1
5 199 5 732 30 -1
6 272 13 876 152 -1
7 83 40 901 18 -1
8 222 2134 933 64 -1
9 118 8 1176 60 -1

Estas variables, registradas como 'other' la funcion no ha sabido categorizarlas como numéricas o categóricas. Consideramos que las variables son numéricas.

Preprocesamiento inicial de algunas variables¶

No hemos necesitado el procesamiento de variables como los meses, puesto que ya estaban tratadas y categorizadas previamente. Es el caso de month, que podría ser una variable categorizable según este código pero ya se encontraba tratada.

In [28]:
df_base_filter_null.to_csv("./01_processed.csv")